﻿#region MIT
/*
Blue.ResourceManager
Part of Engine Of Evermore (http://code.google.com/p/engineofevermore/)
Copyright (c) 2008 Michael Woerister

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#endregion

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Diagnostics;

namespace Blue.ResourceManager
{
    public abstract class AbstractResourceManager : IDisposable
    {
        #region Variables
        bool _disposed;
        StringBuilder _normalizedNameBuilder = new StringBuilder( 256 );
        protected Dictionary<String, WeakReference> _resourceCache = new Dictionary<String, WeakReference>();
        protected Dictionary<String, CreateResourceDelegate> _factories = new Dictionary<String, CreateResourceDelegate>();
        #endregion

        #region Events
        internal event EventHandler<ResourceRenamedEventArgs> ResourceRenamed;
        internal event EventHandler<ResourceCreatedEventArgs> ResourceCreated;
        internal event EventHandler<ResourceDeletedEventArgs> ResourceDeleted;
        internal event EventHandler<ResourceRenamedEventArgs> ResourceDirectoryRenamed;
        internal event EventHandler<ResourceCreatedEventArgs> ResourceDirectoryCreated;
        internal event EventHandler<ResourceDeletedEventArgs> ResourceDirectoryDeleted;
        #endregion

        #region Properties
        public abstract bool IsReadOnly { get; }

        public bool IsDisposed { get { return _disposed; } }
        #endregion

        #region Public Methods
        public void RegisterResourceFactory( String ext,
            CreateResourceDelegate factoryFunction )
        {
            Debug.Assert( !_factories.ContainsKey( ext ) );
            Debug.Assert( factoryFunction != null );

            if ( ext[0] != '.' )
            {
                ext = "." + ext;
            }
            
            _factories.Add( ext, factoryFunction );
        }


        /// <summary>
        /// Implements IResourceManager.GetResource()
        /// </summary>
        public T GetResource<T>( String name ) where T : Resource
        {
            name = NormalizeName( name );

            T loadedResource = GetResourceIfLoaded<T>( name );

            if ( loadedResource != null )
            {
                return loadedResource;
            }

            String extension = Path.GetExtension( name );

            using ( Stream stream = OpenStream( name, ResourceOpenMode.Read ) )
            {
                CreateResourceDelegate factoryFunction;
                if ( _factories.TryGetValue( extension, out factoryFunction ) )
                {
                    Resource resource = factoryFunction( name, stream );

                    _resourceCache.Add( name, new WeakReference( resource ) );

                    return (T) resource;
                }
            }

            throw new ResourceManagerException(
                "There is no factory for the file extension " + extension + "!",
                ResourceManagerException.Reason.NO_FACTORY_REGISTERED 
                );
        }


        public T GetResourceIfLoaded<T>( String name ) where T : Resource
        {
            name = NormalizeName( name );

            WeakReference resourceRef;

            if ( _resourceCache.TryGetValue( name, out resourceRef ) )
            {
                Object resource = resourceRef.Target;
                
                if ( resource != null )
                {
                    try
                    {
                        return (T) resource;
                    }
                    catch ( InvalidCastException ex )
                    {
                        throw new ResourceManagerException(
                            "The resource is not of type " + typeof( T ).ToString(),
                            ex,
                            ResourceManagerException.Reason.WRONG_RESOURCE_TYPE
                            );
                    }
                }
                else
                {
                    _resourceCache.Remove( name );
                }
            }

            return null;
        }


        /// <summary>
        /// Returns whether the resource is currently loaded.
        /// </summary>
        /// <param directoryName="directoryName"></param>
        /// <returns></returns>
        public bool IsResourceLoaded( String name )
        {
            name = NormalizeName( name );
            return _resourceCache.ContainsKey( name );
        }


        public void RenameResource( String oldName, String newName )
        {
            oldName = NormalizeName( oldName );
            newName = NormalizeName( newName );

            if ( !ResourceExists( oldName ) )
            {
                throw new ResourceManagerException( "Resource " + oldName + " does not exist",
                    ResourceManagerException.Reason.RESOURCE_NOT_FOUND );
            }

            if ( ResourceExists( newName ) )
            {
                throw new ResourceManagerException( "Resource " + newName + " already exists",
                    ResourceManagerException.Reason.RESOURCE_ALREADY_EXISTS );
            }

            _renameResource( oldName, newName );

            // If the resource is currently loaded, change directoryName and
            // cache entry
            WeakReference resourceRef;
            if ( _resourceCache.TryGetValue( oldName, out resourceRef ) )
            {
                Object resource = resourceRef.Target;

                _resourceCache.Remove( oldName );

                if ( resource != null )
                {
                    _resourceCache.Add( newName, resourceRef );

                    ( (Resource) resource ).ResourceName = newName;
                }
            }

            _raiseResourceRenamedEvent( oldName, newName );
        }

        
        public Stream OpenStream( String normalizedName, ResourceOpenMode openMode )
        {
            if ( !ResourceExists( normalizedName ) )
            {
                throw new ResourceManagerException(
                    "A resource with name " + normalizedName + " does not exist.",
                    ResourceManagerException.Reason.RESOURCE_NOT_FOUND
                    );
            }

            return _openStream( normalizedName, openMode );    
        }

        
        public String NormalizeName( String name )
        {
            if ( String.IsNullOrEmpty( name ) )
            {
                return name;
            }

            _normalizedNameBuilder.Length = 0;
            _normalizedNameBuilder.Append( name );
            _normalizedNameBuilder.Replace( '/', Path.DirectorySeparatorChar );

            if ( _normalizedNameBuilder[0] == ' ' ||
                 _normalizedNameBuilder[0] == '\t' )
            {
                // Trim beginning
                for ( int i = 0; i < _normalizedNameBuilder.Length; ++i )
                {
                    if ( _normalizedNameBuilder[i] != ' ' &&
                         _normalizedNameBuilder[i] != '\t' )
                    {
                        _normalizedNameBuilder.Remove( 0, i );
                        break;
                    }
                }
            }

            int last = _normalizedNameBuilder.Length - 1;

            if ( _normalizedNameBuilder[last] == ' ' ||
                 _normalizedNameBuilder[last] == '\t' )
            {
                // Trim end
                for ( int i = last; i >= 0; --i )
                {
                    if ( _normalizedNameBuilder[i] != ' ' &&
                         _normalizedNameBuilder[i] != '\t' )
                    {
                        _normalizedNameBuilder.Remove( i+1, last - i );
                        break;
                    }
                }
            }

            if ( _normalizedNameBuilder[0] == Path.DirectorySeparatorChar )
            {
                _normalizedNameBuilder.Remove( 0, 1 );
            }

            if ( _normalizedNameBuilder[_normalizedNameBuilder.Length - 1]
                == Path.DirectorySeparatorChar )
            {
                _normalizedNameBuilder.Length -= 1;
            }

            return _normalizedNameBuilder.ToString();
        }
        

        public ResourceCreationStream CreateResource( String name )
        {
            name = NormalizeName( name );

            if ( ResourceExists( name ) )
            {
                throw new ResourceManagerException(
                    "A resource with the name " + name + " already exists",
                    ResourceManagerException.Reason.RESOURCE_ALREADY_EXISTS
                    );
            }
            else
            {
                Stream stream = 
                    _openStream( name, ResourceOpenMode.TruncateAndWrite );
                return new ResourceCreationStream( stream, name );
            }
        }


        public void CreateResourceDirectory( String name )
        {
            _createResourceDirectory( name );

            if ( ResourceDirectoryCreated != null )
            {
                ResourceCreatedEventArgs e =
                    new ResourceCreatedEventArgs( name );

                ResourceDirectoryCreated( this, e );
            }
        }


        public void RenameResourceDirectory( String oldName, String newName )
        {
            oldName = NormalizeName( oldName );
            newName = NormalizeName( newName );

            if ( !ResourceDirectoryExits( oldName ) )
            {
                throw new ResourceManagerException( "Resource Directory " + oldName + " does not exist",
                    ResourceManagerException.Reason.RESOURCE_NOT_FOUND );
            }

            if ( ResourceDirectoryExits( newName ) )
            {
                throw new ResourceManagerException( "Resource Directory " + newName + " already exists",
                    ResourceManagerException.Reason.RESOURCE_ALREADY_EXISTS );
            }

            _renameResourceDirectory( oldName, newName );

            List<KeyValuePair<String, Resource>> renamedResources =
                new List<KeyValuePair<String, Resource>>();

            foreach ( var entry in _resourceCache )
            {
                if ( entry.Key.StartsWith( oldName ) )
                {
                    Object resource = entry.Value.Target;

                    if ( resource == null )
                        continue;

                    String newResourceName = newName + entry.Key.Substring( oldName.Length );
                    ( (Resource) resource ).ResourceName = newResourceName;

                    renamedResources.Add(
                        new KeyValuePair<String, Resource>(
                            entry.Key,
                            (Resource) resource
                            )
                        );
                }
            }

            foreach ( var entry in renamedResources )
            {
                _resourceCache.Remove( entry.Key );
                _resourceCache.Add( entry.Value.ResourceName,
                    new WeakReference( entry.Value )
                    );
            }

            foreach ( String relResourceName in FindResources( newName, "*" ) )
            {
                String oldResourceName = 
                    oldName + Path.DirectorySeparatorChar + relResourceName;
                String newResourceName = 
                    newName + Path.DirectorySeparatorChar + relResourceName;

                _raiseResourceRenamedEvent( oldResourceName, newResourceName );
            }

            _raiseResourceDirectoryRenamedEvent( oldName, newName );
        }


        public void DeleteResource( String name )
        {
            _deleteResource( name );
            _raiseResourceDeletedEvent( name );
        }


        public void DeleteResourceDirectory( String directoryName )
        {
            directoryName = NormalizeName( directoryName );
            
            if ( !ResourceDirectoryExits( directoryName ) )
            {
                throw new ResourceManagerException(
                    "No resource directory with name " + directoryName +
                    "was found",
                    ResourceManagerException.Reason.RESOURCE_NOT_FOUND
                    );
            }

            List<String> deletedResources = new List<String>(
                FindResources( directoryName, "*" )
                );

            foreach ( String resource in deletedResources )
            {
                String resourceName = directoryName +
                    Path.DirectorySeparatorChar + resource;

                DeleteResource( resourceName );
            }

            _deleteResourceDirectory( directoryName );
            _raiseResourceDirectoryDeletedEvent( directoryName );
        }


        public void Dispose()
        {
            _dispose( true );

            GC.SuppressFinalize( this );
        }

        #endregion

        #region Protected Virtual and Abstract Methods

        protected abstract void _renameResource( String oldName, String newName );

        protected abstract Stream _openStream( 
            String normalizedName, ResourceOpenMode openMode );

        public abstract IEnumerable<String> FindResources( 
            String searchDir, String ext );

        public abstract IEnumerable<String> FindDirectories( String searchDir );

        public abstract bool ResourceExists( String name );

        public abstract bool ResourceDirectoryExits( String name );


        protected abstract void _createResourceDirectory( String name );

        protected abstract void _renameResourceDirectory(
            String oldName, String newName );

        protected abstract void _deleteResource( String name );

        protected abstract void _deleteResourceDirectory( String name );

        public abstract bool IsResourceDirectoryEmpty( String name );

        protected virtual void _dispose( bool disposing )
        {
            if ( !_disposed )
            {
                if ( disposing )
                {
                    while ( _resourceCache.Count != 0 )
                    {
                        IEnumerator<KeyValuePair<String, WeakReference>>
                            it = _resourceCache.GetEnumerator();

                        it.MoveNext();

                        IDisposable disposable =
                            it.Current.Value as IDisposable;

                        if ( disposable != null )
                        {
                            disposable.Dispose();
                        }
                        else
                        {
                            _resourceCache.Remove( it.Current.Key );
                        }
                    }
                }

                _disposed = true;
            }
        }
        #endregion

        #region Internal and Private Methods
        internal protected virtual void OnResourceDisposed( Resource resource )
        {
            _resourceCache.Remove( resource.ResourceName );
        }

        internal void OnResourceCreationStreamDisposed(
            ResourceCreationStream stream )
        {
            if ( stream.ResourceCreationAborted )
            {
                _deleteResource( stream.ResourceName );
            }
            else
                if ( ResourceCreated != null )
                {
                    ResourceCreatedEventArgs e =
                        new ResourceCreatedEventArgs( stream.ResourceName );

                    ResourceCreated( this, e );
                }
        }

        void _raiseResourceRenamedEvent(
            String oldName, String newName )
        {
            if ( ResourceRenamed != null )
            {
                ResourceRenamedEventArgs e =
                    new ResourceRenamedEventArgs( oldName, newName );

                ResourceRenamed( this, e );
            }
        }

        void _raiseResourceDeletedEvent( String name )
        {
            if ( ResourceDeleted != null )
            {
                ResourceDeletedEventArgs e =
                    new ResourceDeletedEventArgs( name );

                ResourceDeleted( this, e );
            }
        }

        void _raiseResourceDirectoryRenamedEvent(
            String oldName, String newName )
        {
            if ( ResourceDirectoryRenamed != null )
            {
                ResourceRenamedEventArgs e =
                    new ResourceRenamedEventArgs( oldName, newName );

                ResourceDirectoryRenamed( this, e );
            }
        }

        void _raiseResourceDirectoryDeletedEvent( String name )
        {
            if ( ResourceDirectoryDeleted != null )
            {
                ResourceDeletedEventArgs e =
                    new ResourceDeletedEventArgs( name );

                ResourceDirectoryDeleted( this, e );
            }
        }
        #endregion
    }
}
